﻿
#If UNDER_DEVELOPMENT Then
Imports System.IO
Imports System.Net
Imports std

'refer: http://www.ietf.org/rfc/rfc1867.txt
'basically, boundary for each section is --boundary
'end mark, --boundary--
'if it's a file, save to a temp file in temppath\guid
'otherwise, set _content with default decoding
Public Class formdataDef
    Public headers As WebHeaderCollection = Nothing
    Private tempfile As String = Nothing
    Private _name As String = Nothing
    Private _content As String = Nothing
    Private originfile As String = Nothing
    Private filestream As FileStream = Nothing
    'do not save file content in memory

    Public Function name() As String
        Return _name
    End Function

    Public Function filename() As String
        Return originfile
    End Function

    Public Function content() As String
        Return _content
    End Function

    Private Sub startWrite()
        assert(filestream Is Nothing, "filestream is not nothing when startWrite.")
        filestream = New FileStream(tempfilename(), FileMode.Create, FileAccess.Write, FileShare.None)
    End Sub

    Private Sub write(ByVal d As Byte)
        assert(Not filestream Is Nothing, "filestream is nothing when write.")
        filestream.WriteByte(d)
    End Sub

    Private Sub write(ByVal v As vector(Of Byte))
        assert(Not filestream Is Nothing, "filestream is nothing when write.")
        assert(Not v Is Nothing, "v is nothing when write.")
        filestream.Write(v.rawdata(), 0, v.size())
    End Sub

    Private Sub finishWriteImpl()
        closestream(filestream)
    End Sub

    Private Sub finishWrite()
        assert(Not filestream Is Nothing, "filestream is nothing when finishWrite.")
        finishWriteImpl()
    End Sub

    'can also get content from tempfilename(), if the file size is big enough.
    Public Function read(ByRef d As bytes) As Boolean
        If Not containsfile() Then
            Return False
        Else
            Dim reader As FileStream = Nothing
            Try
                reader = New FileStream(tempfilename(), FileMode.Open)
            Catch ex As Exception
                raiseError("cannot read file " + tempfilename() + ", ex " + ex.Message, _
                           errorHandle.errorType.exclamation)
                Return False
            End Try

            If d Is Nothing Then
                d = New bytes()
            End If
            d.size() = reader.Length()

            If reader.Read(d.data, 0, reader.Length()) <> reader.Length() Then
                raiseError("do not get enough from file " + tempfilename(), errorHandle.errorType.exclamation)
                Return False
            Else
                Return True
            End If
        End If
    End Function

    'just leave them here, they will be only used in rfc1867, others are handled by .net framework
    Public Const boundaryStart As String = "--"
    Public Shared ReadOnly boundaryStartLen As Int64 = encoding.utf8().GetByteCount(boundaryStart)
    'assume one boundaryStart char is 1 byte in utf8 encoding
    Public Shared ReadOnly boundaryStartSingleChar As Char = strmid(boundaryStart, 0, 1)
    Public Shared ReadOnly boundaryStartSingleByte As Byte = encoding.utf8().GetBytes(boundaryStartSingleChar)(0)
    Public Const sectionEnd As String = "--"
    Public Shared ReadOnly sectionEndLen As Int64 = encoding.utf8().GetByteCount(sectionEnd)
    'also assume one sectionEnd char is 1 byte in utf8 encoding
    Public Shared ReadOnly sectionEndSingleByte As Byte = encoding.utf8().GetBytes(strmid(sectionEnd, 0, 1))(0)
    Public Const headerSeparator As String = character.return + character.newline
    Public Shared ReadOnly headerSeparatorLen As Int64 = encoding.utf8().GetByteCount(headerSeparator)
    'also assume one headerSeparator char is 1 byte in utf8 encoding
    Public Shared ReadOnly headerSeparatorFirstCharByte As Byte = _
                                                        encoding.utf8().GetBytes(strmid(headerSeparator, 0, 1))(0)
    Public Shared ReadOnly headerSeparatorSecondCharByte As Byte = _
                                                        encoding.utf8().GetBytes(strmid(headerSeparator, 1, 1))(0)
    Public Const dispositionSeparator As Char = character.semicolon
    Public Const dispositionNameValueSeparator As Char = character.equalSign
    Public Const dispositionName As String = "name"
    Public Const dispositionFilename As String = "filename"
    Public Const dispositionFormData As String = "form-data"

    'means last read status
    Private Enum status
#If RETIRED Then
        beforeBoundaryStart
        boundaryStart
        boundary
#End If
        header
        content             'for form content only, save in memory, _content
        file                'for file, save in temporary file, tempfile
        beforeBoundaryEnd
    End Enum

    'read one line, end with headerSeparator <\r\n>, or end of stream. do not include \r\n
    Private Shared Function readline(ByVal inputstream As Stream, _
                                    ByRef v As vector(Of Byte), ByRef str As String) As Boolean
        assert(Not inputstream Is Nothing, "inputstream is nothing.")
        If v Is Nothing Then
            v = New vector(Of Byte)()
        Else
            v.clear()
        End If
        Dim b As Int32 = Nothing
        While True
            b = inputstream.ReadByte()
            If b <> headerSeparatorFirstCharByte AndAlso b >= 0 Then
                v.push_back(b)
            Else
                If b = headerSeparatorFirstCharByte Then
                    'read extra \n, check only in debug mode.
                    If isdebugmode() Then
                        b = inputstream.ReadByte()
                        If b <> headerSeparatorSecondCharByte Then
                            raiseError("got invalid header separator from input stream.", errorHandle.errorType.user)
                        End If
                    Else
                        inputstream.ReadByte()
                    End If
                Else
                    Return False
                End If

                str = encoding.utf8().GetString(v.rawdata(), 0, v.size())

                Return True
            End If
        End While
    End Function

    Private Sub parseheader(ByVal s As String)
        Dim index As Int64
        index = strindexof(s, network.headerNameValueSeparator)
        Dim name As String = Nothing
        Dim value As String = Nothing
        If index <> npos Then
            name = strleft(s, index)
            value = strmid(s, index + strlen(network.headerNameValueSeparator))
            kickLeftRightBlank(value)
        Else
            copy(name, s)
        End If

        headers.Add(name, value)

        If strsame(name, network.http.headers.contentDisposition, False) Then
            Dim v As vector(Of String) = Nothing
            strsepar(value, dispositionSeparator + dispositionNameValueSeparator + character.blank, _
                     v, character.quoteMark, True)
            'suppose after this kind of strsepar, should have name / value / name / value in each slot
            'the first should be form-data
            If Not v Is Nothing AndAlso v.size() > 0 Then
                If isdebugmode() AndAlso Not strsame(v(0), dispositionFormData, False) Then
                    raiseError("got invalid formdata, without form-data in Content-Disposition", _
                               errorHandle.errorType.user)
                End If
                Dim i As Int64
                For i = 1 To v.size() - 2 Step 2
                    If strsame(v(i), dispositionName, False) Then
                        copy(_name, v(i + 1))
                    ElseIf strsame(v(i), dispositionFilename, False) Then
                        copy(originfile, v(i + 1))
                    End If
                Next
            ElseIf isdebugmode() Then
                raiseError("got invalid formdata, cannot separate " + network.http.headers.contentDisposition, _
                           errorHandle.errorType.user)
            End If
        End If
    End Sub

    Private Shared Function isBoundaryStartAndBoundary(ByVal s As String, ByVal boundary As String) As Boolean
        Return strsame(s, boundaryStart + boundary, strlen(boundaryStart + boundary), False)
    End Function

    Private Shared Function isSectionEnd(ByVal s As String, ByVal boundary As String) As Boolean
        Return strsame(s, strlen(boundaryStart + boundary), sectionEnd, 0, strlen(sectionEnd), False)
    End Function

    Private Shared Function passStartBoundary(ByVal boundary As String, ByVal inputstream As Stream) As Boolean
        Dim s As String = Nothing
        'finish \r\n in readline
        If readline(inputstream, Nothing, s) Then
            If isdebugmode() AndAlso Not isBoundaryStartAndBoundary(s, boundary) Then
                raiseError("do not get a valid boundary in input stream.", _
                           errorHandle.errorType.user)
                Return False
            Else
                Return True
            End If
        Else
            Return False
        End If
    End Function

    Public Function containsfile() As Boolean
        Return Not isemptystring(filename())
    End Function

    'parse the input stream with boundary.
    'return true is success, if the input stream contains an invalid section, [something likes no name], return false.
    'set finish to true if the next two characters are -- not \r\n after the boundary.
    'save formdatas flat, in a map(of string, formdataDef), the first string is name.
    Private Function parseImpl(ByVal boundary As String, ByVal inputstream As Stream, _
                               ByRef finish As Boolean, ByRef formdatas As map(Of String, formdataDef)) As Boolean
        assert(Not inputstream Is Nothing, "inputstream is nothing.")
        assert(Not formdatas Is Nothing, "formdatas is nothing.")

        'start from header
        'the first boundary will be passed by passStartBoundary
        Dim stat As status = status.header

        While True
            Select Case stat
#If RETIRED Then
                Case status.beforeBoundaryStart
                    Dim b As Int16
                    'read two boundaryStart
                    Dim i As Byte
                    For i = 0 To 1
                        b = inputstream.ReadByte()
                        If isdebugmode() AndAlso b <> boundaryStartSingleByte Then
                            GoTo exterr
                        End If
                    Next
                    stat = status.boundary
                Case status.boundary
                    'read whole boundary
                    'only check whether boundary read is just the input boundary in debug mode, for debug only
                    Dim s As String = Nothing
                    'finish \r\n in readline
                    If readline(inputstream, Nothing, s) Then
                        If isdebugmode() AndAlso Not strsame(boundary, s, True) Then
                            raiseError("do not get a valid boundary in input stream.", _
                                       errorHandle.errorType.user)
                        End If
                    Else
                        GoTo exterr
                    End If
                    stat = status.header
#End If
                Case status.header
                    Dim s As String = Nothing
                    If readline(inputstream, Nothing, s) Then
                        If isemptystring(s) Then
                            'it's an empty line, so jump to content now
                            'three posibilities now,
                            '1. multipart/form-data, recurssion
                            '2. content only, save to content
                            '3. file, save to temp file
                            If isMultipartFormDataContentType(headers(HttpRequestHeader.ContentType)) Then
                                Dim subboundary As String = Nothing
                                parseContentTypeBoundary(headers(HttpRequestHeader.ContentType), subboundary)
                                parse(subboundary, inputstream, formdatas)
                                stat = status.beforeBoundaryEnd
                            ElseIf containsfile() Then
                                startWrite()
                                stat = status.file
                            Else
                                stat = status.content
                            End If
                        Else
                            'it's a valid header, parse it
                            parseheader(s)
                        End If
                    Else
                        GoTo exterr
                    End If
                Case status.content
                    Dim s As String = Nothing
                    If readline(inputstream, Nothing, s) Then
                        If isBoundaryStartAndBoundary(s, boundary) Then
                            formdatas.insert(name(), Me)
                            If isSectionEnd(s, boundary) Then
                                finish = True
                            End If
                            Exit While
                        Else
                            If Not isemptystring(content()) Then
                                _content += headerSeparator
                            End If
                            _content += s
                        End If
                    Else
                        GoTo exterr
                    End If
                Case status.file
                    Dim b As Int16
                    b = inputstream.ReadByte()
                    If b > 0 Then
                        If b = boundaryStartSingleByte Then
                            Dim s As String = Nothing
                            Dim v As vector(Of Byte) = Nothing
                            If readline(inputstream, v, s) Then
                                If isBoundaryStartAndBoundary(boundaryStartSingleChar + s, boundary) Then
                                    finishWrite()
                                    formdatas.insert(name(), Me)
                                    If isSectionEnd(s, boundary) Then
                                        finish = True
                                    End If
                                    Exit While
                                Else
                                    'not a boundary, write to temp file
                                    write(b)
                                    write(v)
                                    write(headerSeparatorFirstCharByte)
                                    write(headerSeparatorSecondCharByte)
                                End If
                            Else
                                GoTo exterr
                            End If
                        Else
                            write(b)
                        End If
                    Else
                        GoTo exterr
                    End If
                Case status.beforeBoundaryEnd
                    Dim s As String = Nothing
                    If readline(inputstream, Nothing, s) Then
                        If Not isBoundaryStartAndBoundary(s, boundary) Then
                            raiseError("get invalid boundary when in beforeBoundaryEnd.", errorHandle.errorType.user)
                        Else
                            If isSectionEnd(s, boundary) Then
                                finish = True
                            End If
                        End If
                        Exit While
                    Else
                        GoTo exterr
                    End If
                Case Else
                    assert(False, "should cover all status.")
                    GoTo exterr
            End Select
        End While

        Return True

exterr:
        finishWriteImpl()
        finish = True
        Return False
    End Function

    Public Function tempfilename() As String
        Return tempfile
    End Function

    Public Sub New()
        headers = New WebHeaderCollection()
        tempfile = filesystem.temppath() + filesystem.directorySeperator + createGUID()
    End Sub

    Public Shared Function parse(ByVal boundary As String, ByVal inputstream As Stream, _
                                 ByRef formdatas As map(Of String, formdataDef)) As Boolean
        If isemptystring(boundary) Then
            raiseError("do not have a valid boundary.", errorHandle.errorType.user)
            Return False
        ElseIf inputstream Is Nothing Then
            raiseError("inputstream is nothing.", errorHandle.errorType.user)
            Return False
        Else
            If Not passStartBoundary(boundary, inputstream) Then
                raiseError("got invalid formdata, cannot find start boundary message from inputstream.", _
                           errorHandle.errorType.user)
                Return False
            Else
                If formdatas Is Nothing Then
                    formdatas = New map(Of String, formdataDef)()
                End If

                Dim finish As Boolean = False
                While Not finish
                    Dim f As formdataDef = Nothing
                    f = New formdataDef()
                    If Not f.parseImpl(boundary, inputstream, finish, formdatas) Then
                        raiseError("cannot parse formdata.", errorHandle.errorType.exclamation)
                    End If
                End While

                Return True
            End If
        End If
    End Function

    Protected Overrides Sub Finalize()
        If File.Exists(tempfilename()) Then
            Try
                File.Delete(tempfilename())
            Catch ex As Exception
                raiseError("cannot delete tempfile, " + tempfilename() + ", ex " + ex.Message, _
                           errorHandle.errorType.warning)
            End Try
        End If
        MyBase.Finalize()
    End Sub
End Class

Public Module rfc1867
    'refer: http://www.ietf.org/rfc/rfc1867.txt
    Public Const boundaryStart As String = "boundary="

    Public Sub parseMultipartInputStream(ByRef formdatas As map(Of String, formdataDef), ByVal inputstream As Stream, _
                                         ByVal boundary As String)
        If formdatas Is Nothing Then
            formdatas = New map(Of String, formdataDef)()
        End If
        If Not formdataDef.parse(boundary, inputstream, formdatas) Then
            raiseError("cannot parse multipart input stream.", errorHandle.errorType.warning)
        End If
    End Sub

    Public Sub parseContentTypeBoundary(ByVal contentType As String, ByRef boundary As String)
        Dim index As Int64
        index = strindexof(contentType, boundaryStart, False)
        If index <> npos Then
            boundary = strmid(contentType, index + strlen(boundaryStart))
        Else
            boundary = Nothing
        End If
    End Sub

    Public Function isMultipartFormDataContentType(ByVal contentType As String) As Boolean
        Return strsame(contentType, network.http.contentType.multipart_form_data, _
                       strlen(network.http.contentType.multipart_form_data), False)
    End Function
End Module
#End If
